package com.swak.mmap;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.swak.mmap.MappedFileAllocator.AllocateTask;
import com.swak.utils.Lists;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * MappedEndlessFile.
 * 
 * @author DELL
 */
@Getter
@Setter
public class MappedEndlessFile {

	private static final Logger log = LoggerFactory.getLogger(MappedEndlessFile.class);

	@ToString
	static class MappedFiles {
		int currentIndex = -1;
		List<MappedFile> files = new ArrayList<>();
		Map<Integer, MappedFile> offsets = new HashMap<>();

		public void add(MappedFile file) {
			this.files.add(file);
			this.offsets.put(file.getFileOffset(), file);
		}

		public MappedFile getByIndex(int index) {
			return this.files.get(index);
		}

		public MappedFile getByOffset(int offset) {
			return this.offsets.get(offset);
		}

		public MappedFile last() {
			return files.isEmpty() ? null : files.get(files.size() - 1);
		}

		public MappedFile current() {
			return files.isEmpty() ? null : files.get(currentIndex);
		}

		public MappedFile next() {
			for (int i = currentIndex + 1; i < files.size(); i++) {
				MappedFile file = this.files.get(i);
				if (!file.isFull()) {
					this.currentIndex = i;
					return file;
				}
			}
			return null;
		}

		public int nextFileOffset() {
			MappedFile file = this.last();
			return file == null ? 0 : file.getFileOffset();
		}

		public int size() {
			return files.size();
		}

		public boolean isEmpty() {
			return files.isEmpty();
		}

		public List<MappedFile> files() {
			return this.files;
		}
	}

	private final String storePath;
	private final int mapedFileSize;
	private final int maxRecordSize;
	private final int flushLeastPages;
	private final MappedFiles mapedFiles = new MappedFiles();
	private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	private final MappedFileAllocator allocator;
	private final MappedFileNaming mappedFileNaming;

	public MappedEndlessFile(String storePath, int mapedFileSize, int maxRecordSize) {
		this(storePath, new MappedFileNaming(), mapedFileSize, maxRecordSize, 1, null);
	}

	public MappedEndlessFile(String storePath, MappedFileNaming mappedFileNaming, int mapedFileSize, int maxRecordSize,
			int flushLeastPages, MappedFileAllocator allocator) {
		this.storePath = storePath;
		this.mapedFileSize = mapedFileSize;
		this.maxRecordSize = maxRecordSize;
		this.mappedFileNaming = mappedFileNaming;
		this.flushLeastPages = flushLeastPages;
		this.allocator = allocator == null ? new MappedFileAllocator() : allocator;
		this.recoverMapedFiles();
		this.allocator.registry(new AllocateTask().setFileSize(mapedFileSize).setMappedFileNaming(mappedFileNaming)
				.setMaxRecordSize(maxRecordSize).setOffset(mapedFiles.nextFileOffset()).setStorePath(storePath));
	}

	private void recoverMapedFiles() {
		File dir = new File(this.storePath);
		File[] files = dir.listFiles((file, name) -> {
			return this.mappedFileNaming.match(name);
		});

		if (files != null) {

			// 保证文件有序
			Arrays.sort(files);

			for (File file : files) {

				// 校验文件大小是否匹配
				if (file.length() != this.mapedFileSize) {
					log.warn(file + "\t" + file.length() + " length not matched message store config value, ignore it");
					return;
				}

				// 恢复队列，并没有真实的恢复
				try {
					int fileOffset = this.mappedFileNaming.fileOffset(file.getName());
					MappedFile mapedFile = new MappedFile(file.getPath(), fileOffset, this.mapedFileSize,
							this.maxRecordSize, this.flushLeastPages);
					this.mapedFiles.add(mapedFile);
					log.info("init " + file.getPath() + " OK");
				} catch (IOException e) {
					log.error("init file " + file + " error", e);
					return;
				}
			}
		}

		if (!this.mapedFiles.isEmpty()) {
			int index = this.mapedFiles.size() - 3;
			if (index < 0) {
				index = 0;
			}
			for (int i = index; i < this.mapedFiles.size(); i++) {
				MappedFile mapedFile = this.mapedFiles.getByIndex(i);
				try {
					mapedFile.recover();
				} catch (IOException e) {
					log.error("recover mappedfile failed, file : " + mapedFile.getFileName(), e);
				}
			}
		}

		// current index.
		if (!this.mapedFiles.isEmpty()) {
			for (int i = 0; i < this.mapedFiles.size(); i++) {
				MappedFile mapedFile = this.mapedFiles.getByIndex(i);
				if (!mapedFile.isFull()) {
					this.mapedFiles.currentIndex = i;
					break;
				}
			}
		}

		if (this.mapedFiles.currentIndex == -1) {
			this.mapedFiles.currentIndex = this.mapedFiles.size() - 1;
		}
	}

	/**
	 * 向 MapedBuffer 追加消息
	 * 
	 * @param data
	 * @return
	 * @throws IOException
	 */
	public AppendResult appendData(final byte[] data) throws IOException {

		// first append data.
		AppendResult result = this.doAppendData(data);

		// check append result and append again.
		switch (result.getStatus()) {
		case PUT_OK:
			break;
		case END_OF_FILE:
			// Create a new file, re-write the message
			result = this.doAppendData(data);
			break;
		default:
			return result;
		}
		return result;
	}

	private AppendResult doAppendData(final byte[] data) throws IOException {
		AppendResult result = null;
		this.readWriteLock.writeLock().lock();
		try {

			// 得到可写入数据的 mapped file
			MappedFile currentFile = this.getAppendAbleFile();
			if (null == currentFile) {
				log.error("create maped file error.");
				return new AppendResult(AppendStatus.FILE_ERROR);
			}

			// append file
			long startTimestamp = System.currentTimeMillis();
			result = currentFile.appendData(data);
			long costs = System.currentTimeMillis() - startTimestamp;
			if (costs > 100) {
				log.info("TRACE PhysicCommitLog appendMessage costs {}", costs);
			}
		} catch (IOException e) {
			log.error("mapedFile appendMessage", e);
			return new AppendResult(AppendStatus.FILE_ERROR);
		} finally {
			this.readWriteLock.writeLock().unlock();
		}
		return result;
	}

	/**
	 * 得到正在写入的 MappedFile
	 * 
	 * @return
	 */
	public MappedFile getAppendAbleFile() {
		MappedFile mapedFileLast = this.mapedFiles.current();
		if (mapedFileLast == null || mapedFileLast.isFull()) {

			// 获取下一个， 如果下一个没有则创建一个新的
			mapedFileLast = this.mapedFiles.next();

			// 别忘了刷盘
			if (mapedFileLast != null) {
				mapedFileLast.flush(0);
			}

			// 创建一个新的 mapped file
			if (mapedFileLast == null || mapedFileLast.isFull()) {
				return this.createMappedFile();
			}
		}

		// 返回可以写的 mapped file
		return mapedFileLast;
	}

	private MappedFile createMappedFile() {
		MappedFile mapedFile = null;
		try {
			mapedFile = this.allocator.allocate(this.storePath);
		} catch (Exception e) {
			log.error("create mapedfile exception", e);
		}

		if (mapedFile != null) {
			this.mapedFiles.add(mapedFile);
			this.mapedFiles.currentIndex = this.mapedFiles.size() - 1;
			log.info("MapedFileQueue add mapedFile {}.", mapedFile.getFileName());
		}
		return mapedFile;
	}

	/**
	 * 全局的扫描文件.
	 * 
	 * @param address 全局的地址
	 * @param count   需要扫描的数据数量
	 * @return 扫描结果
	 * @throws IOException
	 */
	public MappedFileScan scan(long address, int count) throws IOException {
		int offset = (int) (address / this.mapedFileSize);
		int position = (int) (address % this.mapedFileSize);
		int recordCount = count;
		List<MappedRecord> records = Lists.newArrayList();
		MappedFile file = null;
		this.readWriteLock.readLock().lock();
		try {
			while ((file = this.mapedFiles.getByOffset(offset)) != null) {
				MappedFileScan scan = file.scan(position, recordCount);
				records.addAll(scan.getRecords());
				position = (int) scan.getPosition();
				recordCount = recordCount - scan.getRecords().size();
				if (recordCount <= 0) {
					break;
				}
				offset = offset + 1;
				position = 0;
			}
		} finally {
			this.readWriteLock.readLock().unlock();
		}
		return new MappedFileScan().setPosition(offset * this.mapedFileSize + position);
	}

	/**
	 * 关闭队列，队列数据还在，但是不能访问
	 */
	public void shutdown(final long intervalForcibly) {
		this.readWriteLock.readLock().lock();
		try {
			for (MappedFile mf : this.mapedFiles.files()) {
				try {
					mf.shutdown(intervalForcibly);
				} catch (Exception e) {
				}
			}
		} finally {
			this.readWriteLock.readLock().unlock();
		}

		try {
			this.allocator.shutdown();
		} catch (Exception e) {
		}
	}

	@Override
	public String toString() {
		MappedFile current = mapedFiles.current();
		return "MappedFiles [storePath=" + storePath + ", mapedFiles=" + mapedFiles + ", currentFileOffset="
				+ (current == null ? "NONE" : current.getFileName()) + "]";
	}
}